package com.xuecheng.media.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.j256.simplemagic.ContentInfo;
import com.j256.simplemagic.ContentInfoUtil;
import com.xuecheng.base.exception.XueChengPlusException;
import com.xuecheng.base.model.PageParams;
import com.xuecheng.base.model.PageResult;
import com.xuecheng.base.model.RestResponse;
import com.xuecheng.media.mapper.MediaFilesMapper;
import com.xuecheng.media.mapper.MediaProcessMapper;
import com.xuecheng.media.model.dto.QueryMediaParamsDto;
import com.xuecheng.media.model.dto.UploadFileParamsDto;
import com.xuecheng.media.model.dto.UploadFileResultDto;
import com.xuecheng.media.model.po.MediaFiles;
import com.xuecheng.media.model.po.MediaProcess;
import com.xuecheng.media.service.MediaFileService;
import io.minio.*;
import io.minio.errors.*;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.io.*;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @author Mr.M
 * @version 1.0
 * @description TODO
 * @date 2022/9/10 8:58
 */
@Service
@Slf4j
public class MediaFileServiceImpl implements MediaFileService {

    @Resource
    private MediaFileService currentProxy;//为了事务控制

    @Resource
    private MediaFilesMapper mediaFilesMapper;

    @Resource
    private MinioClient minioClient;
    @Resource
    private MediaProcessMapper mediaProcessMapper;

    @Value("${minio.bucket.files}")
    private String bucket_mediafiles;

    @Value("${minio.bucket.videofiles}")
    private String bucket_video;

    @Override
    public PageResult<MediaFiles> queryMediaFiels(Long companyId, PageParams pageParams, QueryMediaParamsDto queryMediaParamsDto) {

        //构建查询条件对象
        LambdaQueryWrapper<MediaFiles> queryWrapper = new LambdaQueryWrapper<>();

        //分页对象
        Page<MediaFiles> page = new Page<>(pageParams.getPageNo(), pageParams.getPageSize());
        // 查询数据内容获得结果
        Page<MediaFiles> pageResult = mediaFilesMapper.selectPage(page, queryWrapper);
        // 获取数据列表
        List<MediaFiles> list = pageResult.getRecords();
        // 获取数据总数
        long total = pageResult.getTotal();
        // 构建结果集
        PageResult<MediaFiles> mediaListResult = new PageResult<>(list, total, pageParams.getPageNo(), pageParams.getPageSize());
        return mediaListResult;

    }

    private String getMimeType(String extension) {//根据扩展名获取mimeType
        if (extension == null) {
            extension = "";
        }
        ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
        String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;//通用mimeType，字节流 "application/octet-stream"
        if (extensionMatch != null) {
            mimeType = extensionMatch.getMimeType();
        }
        return mimeType;
    }

    public boolean addMediaFilesToMinIO(String localFilePath, String mimeType, String bucket, String objectName) {//  文件上传
        try {
            UploadObjectArgs testbucket = UploadObjectArgs.builder()
                    .bucket(bucket)
                    .object(objectName)
                    .filename(localFilePath)
                    .contentType(mimeType)
                    .build();
            minioClient.uploadObject(testbucket);
            log.debug("上传文件到minio成功,bucket:{},objectName:{}", bucket, objectName);
            System.out.println("上传成功");
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            log.error("上传文件到minio出错,bucket:{},objectName:{},错误原因:{}", bucket, objectName, e.getMessage(), e);
            XueChengPlusException.cast("上传文件到文件系统失败");
        }
        return false;

    }

    @Override
    public MediaFiles getFileById(String mediaId) {
        MediaFiles mediaFiles = mediaFilesMapper.selectById(mediaId);
        return mediaFiles;
    }

    //获取文件默认存储目录路径 年/月/日/
    private String getDefaultFolderPath() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        String folder = sdf.format(new Date()).replace("-", "/") + "/";
        return folder;
    }

    //获取文件的md5
    private String getFileMd5(File file) {
//  try (资源声明) 语法
//括号中声明的资源（这里是 FileInputStream）必须实现 AutoCloseable 接口。当 try 代码块执行完毕（无论正常结束还是抛出异常），
// JVM 会自动调用资源的 close() 方法，无需手动编写 finally 块来关闭流。
        try (FileInputStream fileInputStream = new FileInputStream(file)) {
            String fileMd5 = DigestUtils.md5Hex(fileInputStream);
            return fileMd5;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    //    4.44444444444444444444444
//  todo 如果这个事务抛出了异常,抛给spring框架,抛给代理对象,然后代理对象会进行事务回滚,这个时候,如果我们把异常捕获了,没有交给spring框架,那么事务并不会回滚
//如果mediaFiles已经存在,就不会重新录入数据库
    @Transactional//使用 '@Transactional' 注解的方法必须可重写 todo 还要把这个暴露成为接口
    public MediaFiles addMediaFilesToDb(Long companyId, String fileMd5,
                                        UploadFileParamsDto uploadFileParamsDto, String bucket, String objectName) {
        MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);
        if (mediaFiles == null) {
            mediaFiles = new MediaFiles();
            BeanUtils.copyProperties(uploadFileParamsDto, mediaFiles);
            mediaFiles.setId(fileMd5);
            mediaFiles.setCompanyId(companyId);
            mediaFiles.setBucket(bucket);
            mediaFiles.setFilePath(objectName);
            mediaFiles.setFileId(fileMd5);
            mediaFiles.setUrl("/" + bucket + "/" + objectName);
            mediaFiles.setCreateDate(LocalDateTime.now());
            mediaFiles.setChangeDate(LocalDateTime.now());
            mediaFiles.setStatus("1");
            mediaFiles.setAuditMind("002003");
            int insert = mediaFilesMapper.insert(mediaFiles);
//  判断该方法是否可以事务控制必须保证是通过代理对象调用此方法，且此方法上添加了@Transactional注解。
//            int i = 1 /0 ; 这里触发异常也不会回滚,todo 如果在uploadFile方法上添加@Transactional注解，代理对象执行此方法前会开启事务
            if (insert < 1) {
                log.debug("向数据库保存文件失败,bucket:{},objectName:{}", bucket, objectName);
                return null;
            }
//          记录视频的待处理任务 只有在数据库中为空时才会注入
            addWaitingTask(mediaFiles);
            return mediaFiles;
        }
        return mediaFiles;
    }

    private void addWaitingTask(MediaFiles mediaFiles) {
        String filename = mediaFiles.getFilename();
        String extension = filename.substring(filename.lastIndexOf("."));
        String mimeType = getMimeType(extension);
        if ("video/x-msvideo".equals(mimeType)){//视频编码类型是.avi todo 其他类型的视频可以吗
            MediaProcess mediaProcess = new MediaProcess();
//当源对象的 id 是 String 类型，而目标对象的 id 是 Long 类型时，由于 数据类型不兼容，拷贝操作会失败（不会抛出异常，只是默默跳过该字段）
            BeanUtils.copyProperties(mediaFiles,mediaProcess);//id不一样
            mediaProcess.setId(null);
            mediaProcess.setStatus("1");
            mediaProcess.setFailCount(0);
            mediaProcess.setUrl(null);//视频文件最终访问地址
            mediaProcess.setCreateDate(LocalDateTime.now());
            mediaProcessMapper.insert(mediaProcess);
        }
    }


    @Override
//    @Transactional 在有网络访问的东西时,不能用数据库事务控值,不然会导致数据库占用资源比较长,极端情况下可能链接不够
    public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, String localFilePath,String objectName) {
        //  文件名
        String filename = uploadFileParamsDto.getFilename();
//        文件扩展名
        String extension = filename.substring(filename.lastIndexOf("."));
//      获取mimeType类型
        String mimeType = getMimeType(extension);
//        根据当前日期来获取目录 1.1111111
        String defaultFolderPath = getDefaultFolderPath();
//        根据MD5值来确定唯一性 2.22222222
        String fileMd5 = getFileMd5(new File(localFilePath));
//        todo 这里进行更改,原来是传入照片,如果传入了objectName,那么就把静态文件放到minio该目录下
        if (StringUtils.isEmpty(objectName)){//没有传入objectName就按照年月日去存储
            //        相同文件会重新录入minio,但是因为相同图片生成的md5一样,所以文件名相同,会被顶替
           objectName = defaultFolderPath + fileMd5 + extension;
        }
//                  3.333333333333
        boolean b = addMediaFilesToMinIO(localFilePath, mimeType, bucket_mediafiles, objectName);
        if (!b) {
            XueChengPlusException.cast("上传失败");
        }
//        不会重新录入数据库,因为mediaFiles已经存在,针对图片而言 todo 这里不是用代理对象调用此方法,所以事务控制不生效
//        MediaFiles mediaFiles = this.addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket_mediafiles, objectName);
//        4.444444444444444    todo 如果这里有捕获异常,那么事务出现了异常被捕获,没有交给spring框架,那么无法回滚
        MediaFiles mediaFiles = currentProxy.addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket_mediafiles, objectName);
        if (mediaFiles == null) {
            XueChengPlusException.cast("录入数据库失败");
        }
        UploadFileResultDto uploadFileResultDto = new UploadFileResultDto();
        BeanUtils.copyProperties(mediaFiles, uploadFileResultDto);
        return uploadFileResultDto;
    }

    @Override//先看数据库中有没有,再看minio中是否存在
    public RestResponse<Boolean> checkFile(String fileMd5) {
        MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);
        if (mediaFiles != null) {//数据库中存在
            String bucket = mediaFiles.getBucket();
            String filePath = mediaFiles.getFilePath();
            GetObjectArgs getObjectArgs = GetObjectArgs.builder()
                    .bucket(bucket)
                    .object(filePath) //对象名
                    .build();
            try {//      开始获取
                FilterInputStream filterInputStream = minioClient.getObject(getObjectArgs);
                if (filterInputStream != null) {//文件已存在
                    return RestResponse.success(true);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return RestResponse.success(false);
    }

    //得到分块文件的目录
    private String getChunkFileFolderPath(String fileMd5) {//得到分块所在的目录
        return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + "chunk" + "/";
    }

    @Override//chunkIndex第几块分块数据,查询分块是否存在
    public RestResponse<Boolean> checkChunk(String fileMd5, int chunkIndex) {
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);//得到分块所在的目录
        GetObjectArgs getObjectArgs = GetObjectArgs.builder()
                .bucket(bucket_video)
                .object(chunkFileFolderPath + chunkIndex) //对象名
                .build();
        try {//      开始获取
            FilterInputStream filterInputStream = minioClient.getObject(getObjectArgs);
            if (filterInputStream != null) {//文件已存在
                return RestResponse.success(true);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return RestResponse.success(false);
    }

    @Override
    public RestResponse uploadChunk(String fileMd5, int chunk, String localChunkFilePath) {
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
        String mimeType = getMimeType(null);
        boolean b = addMediaFilesToMinIO(localChunkFilePath, mimeType, bucket_video, chunkFileFolderPath + chunk);
        if (!b){
            return RestResponse.validfail(false,"上传分块文件失败");
        }
        return RestResponse.success(true);
    }

    @Override
    public RestResponse mergechunks(Long companyId, String fileMd5, int chunkTotal, UploadFileParamsDto uploadFileParamsDto) {
//        ==================合并分块文件================
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);//分块文件所在目录
//        遍历所有的分块文件
        List<ComposeSource> sources = Stream.iterate(0, i -> ++i).limit(chunkTotal)
                .map(i -> ComposeSource.builder()
                        .bucket(bucket_video)
                        .object(chunkFileFolderPath + i)
                        .build())
                .collect(Collectors.toList());
        String filename = uploadFileParamsDto.getFilename();//源文件名称
        String substring = filename.substring(filename.lastIndexOf("."));
        String objectName = getFilePathByMd5(fileMd5, substring);//合并文件后所存储的位置
        ComposeObjectArgs composeObjectArgs = ComposeObjectArgs.builder()
                .bucket(bucket_video)
                .object(objectName)
                .sources(sources)
                .build();
//        minio默认的分块的文件大小最小为5M,最后一块分块文件除外
        try {
            minioClient.composeObject(composeObjectArgs);
        } catch (Exception e) {
            e.printStackTrace();
            log.error("合并文件出错,bucket:{},objectName:{},错误信息:{}",bucket_video,objectName,e.getMessage());
            return RestResponse.validfail(false,"合并文件出错");
        }
        // 校验合并后的文件和源文件是否一致,一致才==============放入到数据库中==================
        File file = downloadFileFromMinIO(bucket_video, objectName);
        uploadFileParamsDto.setFileSize(file.length());//文件的大小
//        todo 这种写法不用关闭流
        try(FileInputStream fileInputStream = new FileInputStream(file)) {
            String mergeFile_Md5 = DigestUtils.md5Hex(fileInputStream);
            if (!fileMd5.equals(mergeFile_Md5)){
                log.error("校验合并后的文件不一致,原始文件:{},合并文件:{}",fileMd5,mergeFile_Md5);
                RestResponse.validfail(false,"校验合并后的文件不一致");
            }
        } catch (Exception e) {
            e.printStackTrace();
            RestResponse.validfail(false,"校验合并后的文件不一致");
        }
//        ===================将文件信息入库==============  使用代理对象入库,保证事务的一致性
        MediaFiles mediaFiles = currentProxy.addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket_video, objectName);
        if (mediaFiles == null){
            return RestResponse.validfail(false,"文件入库失败");
        }
//        ==================清理分块文件================
        clearChunkFiles(chunkFileFolderPath,chunkTotal);
        return RestResponse.success(true);
    }
    /**
     * 得到合并后的文件的地址
     * @param fileMd5 文件id即md5值
     * @param fileExt 文件扩展名
     * @return
     */
    private String getFilePathByMd5(String fileMd5,String fileExt){
        return   fileMd5.substring(0,1) + "/" + fileMd5.substring(1,2) + "/" + fileMd5 + "/" + fileMd5 + fileExt;
    }

    public File downloadFileFromMinIO(String bucket,String objectName){
        //临时文件
        File minioFile = null;
        FileOutputStream outputStream = null;
        try{
            InputStream stream = minioClient.getObject(GetObjectArgs.builder()
                    .bucket(bucket)
                    .object(objectName)
                    .build());
            //创建临时文件
            minioFile=File.createTempFile("minio", ".merge");
            outputStream = new FileOutputStream(minioFile);
            IOUtils.copy(stream,outputStream);
            return minioFile;
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            if(outputStream!=null){
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }

    /**
     * @param chunkFileFolderPath 分块文件所在目录
     * @param chunkTotal 个数
     */
    private void clearChunkFiles(String chunkFileFolderPath,int chunkTotal){
//        try {
//            List<DeleteObject> deleteObjects = Stream.iterate(0, i -> ++i)
//                    .limit(chunkTotal)
//                    .map(i -> new DeleteObject(chunkFileFolderPath.concat(Integer.toString(i))))
//                    .collect(Collectors.toList());
//
//            RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder().bucket("video").objects(deleteObjects).build();
//            Iterable<Result<DeleteError>> results = minioClient.removeObjects(removeObjectsArgs);
//            results.forEach(r->{
//                DeleteError deleteError = null;
//                try {
//                    deleteError = r.get();
//                } catch (Exception e) {
//                    e.printStackTrace();
//                    log.error("清楚分块文件失败,objectname:{}",deleteError.objectName(),e);
//                }
//            });
//        } catch (Exception e) {
//            e.printStackTrace();
//            log.error("清楚分块文件失败,chunkFileFolderPath:{}",chunkFileFolderPath,e);
//        }
        Iterable<DeleteObject> objects = Stream.iterate(0, i -> ++i).limit(chunkTotal)
                .map(i -> new DeleteObject(chunkFileFolderPath + i))
                .collect(Collectors.toList());
        RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder().bucket(bucket_video).objects(objects).build();
        Iterable<Result<DeleteError>> results = minioClient.removeObjects(removeObjectsArgs);
//  todo  removeObjects 方法返回的是一个延迟执行的迭代器（Iterable<Result<DeleteError>>），必须通过 get() 方法触发实际的删除操作并获取结果。
        results.forEach(f ->{
            try {
                DeleteError deleteError = f.get();
            }catch (Exception e){
                e.printStackTrace();
            }
        });
//1.removeObjects 的返回值特性
//minioClient.removeObjects(...) 方法不会立即执行删除操作，而是返回一个 Iterable<Result<DeleteError>> 类型的迭代器。这个迭代器是 “惰性的”（延迟执行），只有当你迭代它并调用 Result.get() 时，才会真正向 MinIO 服务器发送删除请求并获取该文件的删除结果。
//这类似于数据库的 “批处理执行”—— 先构建批处理任务，只有调用 “执行” 方法时才会实际操作。
//2.Result.get() 的作用
//触发删除操作：对每个 Result 对象调用 get()，才会实际执行该文件的删除请求。
//获取删除结果：get() 方法的返回值 DeleteError 包含删除失败的信息（如果成功删除，DeleteError 为 null 或抛出异常）。如果某个文件删除失败（如不存在、权限不足），get() 会抛出异常或通过 DeleteError 告知错误原因。
//3.不调用 get() 的后果
//如果只遍历 results 而不调用 f.get()，删除操作根本不会执行（因为迭代器未被实际触发），所有文件都会被保留。
//        总结
//  get() 方法的作用不是 “先获取文件”，而是 触发实际的删除操作并获取该文件的删除结果。
//  这是 MinIO SDK 设计的特性（延迟执行模式），通过迭代器 +get() 实现批量操作的效率优化和结果逐行处理。如果跳过 get()，批量删除将不会执行。
    }
}
